from typing import Any, Dict
import os
import json

from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

# Envelope helper
def envelope(data: Any | None = None, code: int = 200, message: str = "") -> Dict[str, Any]:
    return {"data": ({} if data is None else data), "code": code, "message": message}


# Exception handlers registration
def register_exception_handlers(app: FastAPI) -> None:
    @app.exception_handler(HTTPException)
    async def http_exception_handler(request, exc: HTTPException):
        msg = exc.detail if hasattr(exc, 'detail') else str(exc)
        return JSONResponse(status_code=exc.status_code, content=envelope({}, exc.status_code, msg or ""))

    @app.exception_handler(RequestValidationError)
    async def validation_exception_handler(request, exc: RequestValidationError):
        return JSONResponse(status_code=422, content=envelope({"errors": exc.errors()}, 422, "validation error"))

    @app.exception_handler(Exception)
    async def generic_exception_handler(request, exc: Exception):
        return JSONResponse(status_code=500, content=envelope({}, 500, str(exc)))


# LangGraph availability and default engine
try:
    from flow_engine.execution.langgraph_runner import LangGraphRunner  # noqa: F401
    HAS_LANGGRAPH = True
except Exception:
    HAS_LANGGRAPH = False


def _get_default_engine() -> str:
    val = (os.getenv('FLOW_DEFAULT_ENGINE') or '').strip().lower()
    if val in ('lang', 'langgraph'):
        return 'lang' if HAS_LANGGRAPH else 'flow'
    if val in ('flow',):
        return 'flow'
    if val in ('demo',):
        return 'demo'
    # default
    return 'flow'


DEFAULT_ENGINE = _get_default_engine()


# Mermaid helpers
import re

def _sanitize_id(s: str) -> str:
    sid = re.sub(r"[^A-Za-z0-9_]", "_", s)
    if not sid or sid[0].isdigit():
        sid = f"N_{sid}"
    return sid


def to_mermaid_code(flow: Dict[str, Any]) -> str:
    nodes = flow.get('nodes', [])
    edges = flow.get('edges', [])

    id_map: Dict[str, str] = {}
    labels: Dict[str, str] = {}
    types: Dict[str, str] = {}
    branch_label_map: Dict[str, Dict[str, str]] = {}
    for n in nodes:
        nid = n.get('id', '')
        sid = _sanitize_id(nid)
        id_map[nid] = sid
        ntype = n.get('type', '') or ''
        types[nid] = ntype
        data = n.get('data', {}) or {}
        label = data.get('label') or nid
        labels[nid] = label
        if (ntype or '').lower() == 'condition':
            branches = (data.get('branches') or [])
            mapping: Dict[str, str] = {}
            for b in branches:
                bid = b.get('id')
                blabel = b.get('label') or bid
                if bid:
                    mapping[str(bid)] = str(blabel)
            branch_label_map[nid] = mapping

    lines: list[str] = ["flowchart TD"]

    for orig_id, sid in id_map.items():
        label = labels.get(orig_id, orig_id)
        ntype = (types.get(orig_id, '') or '').lower()
        if ntype == 'condition':
            lines.append(f"{sid}{{{label}}}")
        elif ntype in ('start', 'begin'):
            lines.append(f"{sid}(({label}))")
        elif ntype in ('end', 'finish'):
            lines.append(f"{sid}(({label}))")
        else:
            lines.append(f"{sid}[\"{label}\"]")

    for e in edges:
        src = id_map.get(e.get('source', ''), None)
        tgt = id_map.get(e.get('target', ''), None)
        if not src or not tgt:
            continue
        source_handle = e.get('sourceHandle')
        edge_label = None
        src_orig = e.get('source', '')
        if source_handle and src_orig in branch_label_map:
            edge_label = branch_label_map[src_orig].get(str(source_handle))
        if not edge_label and isinstance(source_handle, str) and source_handle.strip():
            edge_label = source_handle
        if edge_label:
            lines.append(f"{src} -->|{edge_label}| {tgt}")
        else:
            lines.append(f"{src} --> {tgt}")

    return "\n".join(lines)


# In-memory runners registry (for stop control)
RUNNERS: Dict[int, Any] = {}


# Export OpenAPI JSON helper
def generate_openapi_json(app: FastAPI) -> None:
    try:
        openapi_schema = app.openapi()
        with open("openapi.json", "w", encoding="utf-8") as f:
            json.dump(openapi_schema, f, indent=2, ensure_ascii=False)
    except Exception:
        # non-critical
        pass